package classpath

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
)

type ClassPath struct {
	BasicClasspath Entry // SAIL_HOME/lib/basic
	SiteClasspath  Entry // SAIL_HOME/lib/site
	UserClasspath  Entry // ./  (or current working path)
}

func (cp *ClassPath) initBasicAndSitePath(opt *CpOption) error {
	home := os.Getenv("SAIL_HOME")
	var errWhy string

	if opt.XpathOption != "" {
		cp.BasicClasspath = newEntry(opt.XpathOption)
	} else {
		// Load from Environment
		if home != "" {
			cp.BasicClasspath = newEntry(filepath.Join(home, "lib", "basic"))
		} else {
			goto fail
		}
	}

	if opt.SitePathOption != "" {
		cp.SiteClasspath = newEntry(opt.SitePathOption)
	} else {
		if home != "" {
			cp.BasicClasspath = newEntry(filepath.Join(home, "lib", "site"))
		} else {
			goto fail
		}
	}

	return nil

fail:
	return errors.New(errWhy)
}

func (cp *ClassPath) initUserClasspath(opt *CpOption) {
	ucp := opt.UserClassPath

	if ucp == "" {
		ucp = "."
	}

	cp.UserClasspath = newEntry(ucp)
}

func (cp *ClassPath) FindAndReadClass(className string) ([]byte, Entry, error) {
	className += ".cls" // sail class file

	if data, entry, err := cp.BasicClasspath.readClass(className); err == nil {
		return data, entry, err
	}

	if data, entry, err := cp.SiteClasspath.readClass(className); err == nil {
		return data, entry, err
	}

	return cp.UserClasspath.readClass(className)
}

func (cp *ClassPath) FindAndReadClassSource(className string) ([]byte, Entry, error) {
	className += ".sail" // sail class file

	if data, entry, err := cp.BasicClasspath.readClass(className); err == nil {
		return data, entry, err
	}

	if data, entry, err := cp.SiteClasspath.readClass(className); err == nil {
		return data, entry, err
	}

	return cp.UserClasspath.readClass(className)
}

func (cp *ClassPath) existsIf(path string) (bool, error) {
	if exists, err := cp.BasicClasspath.classExists(path); err != nil {
		return false, err
	} else if exists {
		return true, nil
	}

	if exists, err := cp.SiteClasspath.classExists(path); err != nil {
		return false, err
	} else if exists {
		return true, nil
	}

	if exists, err := cp.SiteClasspath.classExists(path); err != nil {
		return false, err
	} else {
		return exists, err
	}
}

func (cp *ClassPath) existsForRange(path string) (bool, error) {
	for _, entry := range []Entry{cp.BasicClasspath, cp.SiteClasspath} {
		exists, err := entry.classExists(path)
		if err != nil {
			return false, err
		}
		if exists {
			return true, nil
		}
	}
	return cp.UserClasspath.classExists(path)
}

func (cp *ClassPath) ClassExists(className string) (bool, error) {
	classFileName := className + ".cls"
	classSourceName := className + ".sail"

	existsFunc := cp.existsForRange

	// class source first.
	exists, err := existsFunc(classSourceName)
	if err != nil {
		return false, err
	}

	if exists {
		return true, nil
	}

	exists, err = existsFunc(classFileName)
	if err != nil {
		return false, err
	}

	return exists, nil
}

func (cp *ClassPath) String() string {
	return fmt.Sprintf("ClassPath:\n\tBasic: %v\n\tSite: %v\n\tUser: %v\n",
		cp.BasicClasspath.String(),
		cp.SiteClasspath.String(),
		cp.UserClasspath.String())
}

func (cp *ClassPath) ListDir(path string) ([]string, error) {
	for _, entry := range []Entry{cp.BasicClasspath, cp.SiteClasspath} {
		dirs, err := entry.listDir(path)

		if err == nil {
			return dirs, nil
		}
	}

	return cp.UserClasspath.listDir(path)
}

func GetClasspathByCmdOption(opt *CpOption) (*ClassPath, error) {
	cp := new(ClassPath)

	if err := cp.initBasicAndSitePath(opt); err != nil {
		return nil, err
	}

	cp.initUserClasspath(opt)

	return cp, nil
}
